/*
 * Copyright (C) 2016 Robinhood Markets, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.robinhood.ticker;

import com.robinhood.ticker.util.ResUtil;
import com.robinhood.ticker.util.TextUtils;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.BlurDrawLooper;
import ohos.agp.render.Canvas;
import ohos.agp.render.MaskFilter;
import ohos.agp.render.Paint;
import ohos.agp.text.Font;
import ohos.agp.utils.Color;
import ohos.agp.utils.LayoutAlignment;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.agp.utils.TextAlignment;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;

/**
 * The primary view for showing ticker text view that handles smoothly scrolling from the
 * current text to given text. The scrolling behavior is defined by
 * {@link #setCharacterLists} which dictates what characters come in between the starting
 * and ending characters.
 *
 * <p>This class primarily handles the drawing customization of the ticker view, for example
 * setting animation duration, interpolator, colors, etc. It ensures that the canvas is properly
 * positioned, and then it delegates the drawing of each column of text to
 * {@link TickerColumnManager}.
 *
 * <p>This class's API should behave similarly to that of {@link ohos.agp.components.Text}.
 * However, We chose to extend from {@link Component} instead of {@link ohos.agp.components.Text}
 * because it allows me full flexibility in customizing the drawing and also support different
 * customization attributes as they are implemented.
 *
 * @author Jin Cao, Robinhood
 */
public class TickerView extends Component implements Component.DrawTask {
    private static final float FLOAT_2 = 2f;

    private static final int DEFAULT_ANIMATION_DURATION = 500;

    private static final int DEFAULT_TEXT_SIZE = 12;

    private static final int DEFAULT_TEXT_COLOR = Color.BLACK.getValue();

    private static final int DEFAULT_TEXT_ALIGNMENT = TextAlignment.CENTER;

    private static final String DEFAULT_SCROLLING_DIRECTION = "any";

    private static final String DEFAULT_CHARACTER_LIST = "number";

    private static final int DEFAULT_ANIMATION_INTERPOLATOR = Animator.CurveType.ACCELERATE_DECELERATE;

    private String mText;

    private String pendingTextToSet;

    private int mTextSize;

    private int mTextColor;

    private int mLayoutAlignment;

    private String mScrollingDirection;

    private String mDefaultCharacterList;

    private Paint textPaint;

    private TickerDrawMetrics metrics;

    private TickerColumnManager columnManager;

    private final Rect viewBounds = new Rect();

    /**
     * Scrolling direction for tickerView
     */
    public enum ScrollingDirection {
        /**
         * Scrolling Direction Any
         */
        ANY,
        /**
         * Scrolling Direction Up
         */
        UP,
        /**
         * Scrolling Direction Down
         */
        DOWN
    }

    /**
     * Flags attributes for the Paint
     */
    public enum PaintFlag {
        /**
         * Sets whether to add an underline for this text.
         */
        UNDERLINE,
        /**
         * Sets whether to add strikethrough for this text.
         */
        STRIKE_THOUGH,
        /**
         * Sets whether to enable sub-pixel anti-aliasing for improving text legibility on an LCD screen.
         */
        SUB_PIXEL_ANTI_ALIAS,
        /**
         * Sets the Dither attribute for Paint.
         */
        DITHER,
        /**
         * Sets the FilterBitmap attribute for Paint.
         */
        FILTER_BITMAP,
        /**
         * Sets the anti-aliasing for Paint object.
         */
        ANTI_ALIAS,
        /**
         * Sets the FakeBoldText attribute for Paint.
         */
        FAKE_BOLD_TEXT,
        /**
         * Sets whether text is displayed in multiple lines.
         */
        MULTIPLE_LINE
    }

    private final AnimatorValue animator = new AnimatorValue();

    private long mAnimationDelayInMillis;

    private long mAnimationDuration;

    private int animationInterpolator;

    private boolean isAnimateMeasurementChange;

    private Color mShadowColor;

    private float mShadowDx;

    private float mShadowDy;

    private float mShadowRadius;

    public TickerView(Context context) {
        this(context, null);
    }

    public TickerView(Context context, AttrSet attrs) {
        this(context, attrs, 0);
    }

    public TickerView(Context context, AttrSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    private void init(AttrSet attrs) {
        final StyledAttributes styledAttributes = new StyledAttributes(attrs);

        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        metrics = new TickerDrawMetrics(textPaint);
        columnManager = new TickerColumnManager(metrics);

        animationInterpolator = DEFAULT_ANIMATION_INTERPOLATOR;
        isAnimateMeasurementChange = false;

        setTextColor(mTextColor);
        setTextSize(mTextSize);
        setDefaultCharacterList();
        setScrollingDirection();
        setShadow();

        String text = ResUtil.getStringFromAttrSet(attrs, styledAttributes.tickerText,
            TickerUtils.provideAlphabeticalList());

        if (isCharacterListsSet()) {
            setText(text, false);
        } else {
            pendingTextToSet = text;
        }

        initAnimation();
        addDrawTask(this);
    }

    private void setShadow() {
        textPaint.setBlurDrawLooper(new BlurDrawLooper(mShadowRadius, mShadowDx, mShadowDy, mShadowColor));
    }

    private void initAnimation() {
        animator.setValueUpdateListener((animatorValue, progress) -> {
            columnManager.setAnimationProgress(progress);
            checkForRelayout();

            new EventHandler(EventRunner.getMainEventRunner()).postTask(() -> invalidate());
        });

        animator.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator1) {
                /* do nothing */
            }

            @Override
            public void onStop(Animator animator1) {
                columnManager.onAnimationEnd();
                checkForRelayout();
                new EventHandler(EventRunner.getMainEventRunner()).postTask(() -> invalidate());
            }

            @Override
            public void onCancel(Animator animator1) {
                /* do nothing */
            }

            @Override
            public void onEnd(Animator animator1) {
                /* do nothing */
            }

            @Override
            public void onPause(Animator animator1) {
                /* do nothing */
            }

            @Override
            public void onResume(Animator animator1) {
                /* do nothing */
            }
        });
    }

    private void setScrollingDirection() {
        switch (mScrollingDirection) {
            case "any":
                metrics.setPreferredScrollingDirection(ScrollingDirection.ANY);
                break;
            case "up":
                metrics.setPreferredScrollingDirection(ScrollingDirection.UP);
                break;
            case "down":
                metrics.setPreferredScrollingDirection(ScrollingDirection.DOWN);
                break;
            default:
                throw new IllegalArgumentException(
                    "Unsupported ticker_defaultPreferredScrollingDirection: " + mScrollingDirection);
        }
    }

    private void setDefaultCharacterList() {
        switch (mDefaultCharacterList) {
            case "number":
                setCharacterLists(TickerUtils.provideNumberList());
                break;
            case "alphabet":
                setCharacterLists(TickerUtils.provideAlphabeticalList());
                break;
            default:
                throw new IllegalArgumentException("Unsupported ticker_defaultCharacterList: " + mDefaultCharacterList);
        }
    }

    /**
     * Re-initialize all of our variables that are dependent on the TextPaint measurements.
     */
    private void onTextPaintMeasurementChanged() {
        metrics.invalidate();
        checkForRelayout();
    }

    private void checkForRelayout() {
        new EventHandler(EventRunner.getMainEventRunner()).postTask(() -> postLayout());
    }

    /* ********* BEGIN PUBLIC API ********* */

    /**
     * This is the primary class that Ticker uses to determine how to animate from one character
     * to another. The provided strings dictates what characters will appear between
     * the start and end characters.
     *
     * <p>For example, given the string "abcde", if the view wants to animate from 'd' to 'b',
     * it will know that it has to go from 'd' to 'c' to 'b', and these are the characters
     * that show up during the animation scroll.
     *
     * <p>We allow for multiple character lists, and the character lists will be prioritized with
     * latter lists given higher priority than the previous lists. e.g. given "123" and "13",
     * an animation from 1 to 3 will use the sequence [1,3] rather than [1,2,3].
     *
     * <p>You can find some helpful character list in {@link TickerUtils}.
     *
     * @param characterLists the list of character lists that dictates animation.
     */
    public void setCharacterLists(String... characterLists) {
        columnManager.setCharacterLists(characterLists);
        if (pendingTextToSet != null) {
            setText(pendingTextToSet, false);
            pendingTextToSet = null;
        }
    }

    /**
     * Can use this value to determine if you need to call {@link #setCharacterLists}
     * before calling {@link #setText}.
     *
     * @return whether or not the character lists (via {@link #setCharacterLists}) have been set.
     */
    public boolean isCharacterListsSet() {
        return columnManager.getCharacterLists() != null;
    }

    /**
     * Get the last set text on the view. This does not equate to the current shown text on the
     * UI because the animation might not have started or finished yet.
     *
     * @return last set text on this view.
     */
    public String getText() {
        return mText;
    }

    /**
     * Sets the string value to display. If the TickerView is currently empty, then this method
     * will immediately display the provided text. Otherwise, it will run the default animation
     * to reach the provided text.
     *
     * @param text the text to display.
     */
    public void setText(String text) {
        setText(text, !TextUtils.isEmpty(mText));
    }

    /**
     * Similar to {@link #setText(String)} but provides the optional argument of whether to
     * animate to the provided text or not.
     *
     * @param text the text to display.
     * @param isAnimate whether to animate to text.
     */
    public void setText(String text, boolean isAnimate) {
        if (TextUtils.equals(text, mText)) {
            return;
        }
        mText = text;
        if (animator.isRunning()) {
            animator.cancel();
        }

        mText = text;
        final char[] targetTexts = text == null ? new char[0] : text.toCharArray();

        columnManager.setText(targetTexts);
        setComponentDescription(text);
        if (isAnimate) {
            // Kick off the animator that draws the transition
            animator.setDelay(mAnimationDelayInMillis);
            animator.setDuration(mAnimationDuration);
            animator.setCurveType(animationInterpolator);
            new EventHandler(EventRunner.getMainEventRunner()).postTask(() -> animator.start());
        } else {
            columnManager.setAnimationProgress(1f);
            columnManager.onAnimationEnd();
            checkForRelayout();
            new EventHandler(EventRunner.getMainEventRunner()).postTask(() -> invalidate());
        }
    }

    /**
     * Gets the current color of text
     *
     * @return the current text color that's being used to draw the text.
     */
    public int getTextColor() {
        return mTextColor;
    }

    /**
     * Sets the text color used by this view. The default text color is defined by
     * {@link #DEFAULT_TEXT_COLOR}.
     *
     * @param color the color to set the text to.
     */
    public void setTextColor(int color) {
        textPaint.setColor(new Color(color));
    }

    /**
     * Return the current text size.
     *
     * @return the current text size that's being used to draw the text.
     */
    public float getTextSize() {
        return mTextSize;
    }

    /**
     * Sets the text size used by this view. The default text size is defined by
     * {@link #DEFAULT_TEXT_SIZE}.
     *
     * @param textSize the text size in pixel units.
     */
    public void setTextSize(float textSize) {
        textPaint.setTextSize((int) textSize);
        onTextPaintMeasurementChanged();
    }

    /**
     * Returns the Font of the text
     *
     * @return the current text typeface.
     */
    public Font getTypeface() {
        return textPaint.getFont();
    }

    /**
     * Sets the typeface size used by this view.
     *
     * @param font the typeface to use on the text.
     */
    public void setTypeface(Font font) {
        textPaint.setFont(font);
        onTextPaintMeasurementChanged();
        new EventHandler(EventRunner.getMainEventRunner()).postTask(() -> invalidate());
    }

    /**
     * Gets the animation delay
     *
     * @return the delay in milliseconds before the transition animations runs
     */
    public long getAnimationDelay() {
        return mAnimationDelayInMillis;
    }

    /**
     * Sets the delay in milliseconds before this TickerView runs its transition animations. The
     * default animation delay is 0.
     *
     * @param animationDelayInMillis the delay in milliseconds.
     */
    public void setAnimationDelay(long animationDelayInMillis) {
        mAnimationDelayInMillis = animationDelayInMillis;
    }

    /**
     * Gets the duration in milliseconds that this TickerView runs its transition animations.
     *
     * @return the duration in milliseconds that the transition animations run for.
     */
    public long getAnimationDuration() {
        return mAnimationDuration;
    }

    /**
     * Sets the duration in milliseconds that this TickerView runs its transition animations. The
     * default animation duration is defined by {@link #DEFAULT_ANIMATION_DURATION}.
     *
     * @param animationDurationInMillis the duration in milliseconds.
     */
    public void setAnimationDuration(long animationDurationInMillis) {
        mAnimationDuration = animationDurationInMillis;
    }

    /**
     * Returns the AnimationInterpolator
     *
     * @return the interpolator used to interpolate the animated values.
     */
    public int getAnimationInterpolator() {
        return animationInterpolator;
    }

    /**
     * Sets the interpolator for the transition animation. The default interpolator is defined by
     * {@link #DEFAULT_ANIMATION_INTERPOLATOR}.
     *
     * @param animationInterpolator the interpolator for the animation.
     */
    public void setAnimationInterpolator(int animationInterpolator) {
        this.animationInterpolator = animationInterpolator;
    }

    /**
     * Sets the preferred scrolling direction for ticker animations.
     * Eligible params include {@link ScrollingDirection#ANY}, {@link ScrollingDirection#UP}
     * and {@link ScrollingDirection#DOWN}.
     * <p>
     * The default value is {@link ScrollingDirection#ANY}.
     *
     * @param direction the preferred {@link ScrollingDirection}
     */
    public void setPreferredScrollingDirection(ScrollingDirection direction) {
        metrics.setPreferredScrollingDirection(direction);
    }

    /**
     * Returns the Layout alignment
     *
     * @return the current text gravity used to align the text. Should be one of the values defined
     * in {@link LayoutAlignment}.
     */
    public int getGravity() {
        return mLayoutAlignment;
    }

    /**
     * Sets the gravity used to align the text.
     *
     * @param gravity the new gravity, should be one of the values defined in
     * {@link LayoutAlignment}.
     */
    public void setGravity(int gravity) {
        if (mLayoutAlignment != gravity) {
            mLayoutAlignment = gravity;
        }
    }

    /**
     * Enables/disables the flag to animate measurement changes. If this flag is enabled, any
     * animation that changes the content's text width (e.g. 9999 to 10000) will have the view's
     * measured width animated along with the text width. However, side effect of this is that
     * the entering/exiting character might get truncated by the view's view bounds as the width
     * shrinks or expands.
     *
     * <p>Warning: using this feature may degrade performance as it will force re-measure and
     * re-layout during each animation frame.
     *
     * <p>This flag is disabled by default.
     *
     * @param isAnimateMeasurementChanged whether or not to animate measurement changes.
     */
    public void setAnimateMeasurementChange(boolean isAnimateMeasurementChanged) {
        isAnimateMeasurementChange = isAnimateMeasurementChanged;
    }

    /**
     * Returns whether the animating measurement changes
     *
     * @return whether or not we are currently animating measurement changes.
     */
    public boolean getAnimateMeasurementChange() {
        return isAnimateMeasurementChange;
    }

    /**
     * Adds custom {@link AnimatorValue.ValueUpdateListener} to listen to animator
     * update events used by this view.
     *
     * @param animatorListener the custom animator listener.
     */
    public void addAnimatorListener(AnimatorValue.ValueUpdateListener animatorListener) {
        animator.setValueUpdateListener(animatorListener);
    }

    /**
     * Removes the ValueUpdateListener from this view.
     *
     * @param animatorListener the custom animator listener.
     */
    public void removeAnimatorListener(AnimatorValue.ValueUpdateListener animatorListener) {
        animator.setValueUpdateListener(null);
    }

    /**
     * Configures the textpaint used for drawing individual ticker characters.
     *
     * @param flag the new flag bits for the paint
     * @param isEnable flag to enable or disable the PaintFlag
     */
    public void setPaintFlags(PaintFlag flag, boolean isEnable) {
        switch (flag) {
            case UNDERLINE:
                textPaint.setUnderLine(isEnable);
                break;
            case STRIKE_THOUGH:
                textPaint.setStrikeThrough(isEnable);
                break;
            case SUB_PIXEL_ANTI_ALIAS:
                textPaint.setSubpixelAntiAlias(isEnable);
                break;
            case DITHER:
                textPaint.setDither(isEnable);
                break;
            case FILTER_BITMAP:
                textPaint.setFilterBitmap(isEnable);
                break;
            case ANTI_ALIAS:
                textPaint.setAntiAlias(isEnable);
                break;
            case FAKE_BOLD_TEXT:
                textPaint.setFakeBoldText(isEnable);
                break;
            case MULTIPLE_LINE:
                textPaint.setMultipleLine(isEnable);
                break;
            default:
                break;
        }
        onTextPaintMeasurementChanged();
    }

    /**
     * Exposing method to add or remove blur mask filter to ticker text.
     *
     * @param style Blur mask filter type
     * @param radius Density of filter
     */
    public void setBlurMaskFilter(MaskFilter.Blur style, float radius) {
        if (style != null && radius > 0f) {
            MaskFilter filter = new MaskFilter(radius, style);
            textPaint.setMaskFilter(filter);
        } else {
            textPaint.setMaskFilter(null);
        }
    }

    /* *******END  PUBLIC API ********* */

    private void realignAndClipCanvasForGravity(Component component, Canvas canvas) {
        final float currentWidth = columnManager.getCurrentWidth();
        final float currentHeight = metrics.getCharHeight();
        viewBounds.set(component.getPaddingLeft(), getPaddingTop(), component.getWidth() - getPaddingRight(),
            component.getHeight() - getPaddingBottom());
        realignAndClipCanvasForGravity(canvas, mLayoutAlignment, viewBounds, currentWidth, currentHeight);
    }

    private static void realignAndClipCanvasForGravity(Canvas canvas, int gravity, Rect viewBounds, float currentWidth,
        float currentHeight) {
        final int availableWidth = viewBounds.getWidth();
        final int availableHeight = viewBounds.getHeight();

        float translationX = 0;
        float translationY = 0;
        if ((gravity & LayoutAlignment.VERTICAL_CENTER) == LayoutAlignment.VERTICAL_CENTER) {
            translationY = viewBounds.top + (availableHeight - currentHeight) / FLOAT_2;
        }
        if ((gravity & LayoutAlignment.HORIZONTAL_CENTER) == LayoutAlignment.HORIZONTAL_CENTER) {
            translationX = viewBounds.left + (availableWidth - currentWidth) / FLOAT_2;
        }
        if ((gravity & LayoutAlignment.TOP) == LayoutAlignment.TOP) {
            translationY = 0;
        }
        if ((gravity & LayoutAlignment.BOTTOM) == LayoutAlignment.BOTTOM) {
            translationY = viewBounds.top + (availableHeight - currentHeight);
        }
        if ((gravity & LayoutAlignment.START) == LayoutAlignment.START) {
            translationX = 0;
        }
        if ((gravity & LayoutAlignment.END) == LayoutAlignment.END) {
            translationX = viewBounds.left + (availableWidth - currentWidth);
        }

        canvas.translate(translationX, translationY);
        canvas.clipRect(new RectFloat(0f, 0f, currentWidth, currentHeight));
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        canvas.save();
        realignAndClipCanvasForGravity(component, canvas);

        // canvas.drawText writes the text on the baseline so we need to translate beforehand.
        canvas.translate(0f, metrics.getCharBaseline());
        columnManager.draw(canvas, textPaint);
        canvas.restore();
    }

    /**
     * Read the values from xml attributes
     */
    private class StyledAttributes {
        private String tickerTextSize = "text_size";

        private final String tickerTextColor = "text_color";

        private String tickerPreferredScrollingDirection = "ticker_defaultPreferredScrollingDirection";

        private String tickerAnimationDuration = "ticker_animationDuration";

        private String tickerLayoutAlignment = "layout_alignment";

        private String tickerDefaultCharacterList = "ticker_defaultCharacterList";

        private String tickerText = "text";

        private String shadowColor = "shadowColor";

        private String shadowDx = "shadowDx";

        private String shadowDy = "shadowDy";

        private String shadowRadius = "shadowRadius";

        StyledAttributes(AttrSet attrs) {
            mTextSize = (int) ResUtil.getDimenFromAttrSet(attrs, tickerTextSize, DEFAULT_TEXT_SIZE);

            mTextColor = ResUtil.getColorFromAttrSet(attrs, tickerTextColor, DEFAULT_TEXT_COLOR);

            mAnimationDuration = ResUtil.getIntFromAttrSet(attrs, tickerAnimationDuration, DEFAULT_ANIMATION_DURATION);

            mLayoutAlignment = ResUtil.getIntFromAttrSet(attrs, tickerLayoutAlignment, DEFAULT_TEXT_ALIGNMENT);

            mDefaultCharacterList = ResUtil.getStringFromAttrSet(attrs, tickerDefaultCharacterList,
                DEFAULT_CHARACTER_LIST);

            mScrollingDirection = ResUtil.getStringFromAttrSet(attrs, tickerPreferredScrollingDirection,
                DEFAULT_SCROLLING_DIRECTION);

            mShadowColor = ResUtil.getColorFromAttrSet(attrs, shadowColor, Color.BLACK);

            mShadowDx = ResUtil.getIntFromAttrSet(attrs, shadowDx, 0);

            mShadowDy = ResUtil.getIntFromAttrSet(attrs, shadowDy, 0);

            mShadowRadius = ResUtil.getIntFromAttrSet(attrs, shadowRadius, 0);
        }
    }
}